Skip to content

net.quic/net.http: HTTP/3 server support, Phase 13 (13a-13d-1) - #28164

Draft
quaesitor-scientiam wants to merge 10 commits into
vlang:masterfrom
quaesitor-scientiam:http3-quic-server-handshake
Draft

net.quic/net.http: HTTP/3 server support, Phase 13 (13a-13d-1)#28164
quaesitor-scientiam wants to merge 10 commits into
vlang:masterfrom
quaesitor-scientiam:http3-quic-server-handshake

Conversation

@quaesitor-scientiam

@quaesitor-scientiam quaesitor-scientiam commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

Summary

Phase 13 (server support) for HTTP/3, tracked at #27675. Sub-phases 13a, 13b,
13c, and 13d-1 are done so far, following the suggested 13a-13e sequence from
the scoping comment on #27675 — same one-sub-phase-per-stacked-PR convention
Phase 12 used for 12a-12d. 13d was split into 13d-1 (this section) and 13d-2
once 13d-1 turned out to be substantially larger than originally scoped:
QuicConn had zero server-role support going in (dial() hardcoded
.client, the handshake dispatch only knew the client's flow), so wiring a
real server-role handshake path in was its own sub-phase before the UDP
listener/demux (13d-2) has anything to call.

The client (Phases 0-12) merged in #28129. Its core QUIC layer is already
role-parameterized (role QuicRole on QuicConn; is_locally_initiated/
initial_*_limit_for_stream already take a role explicitly), so this phase
is additive on top of it, not a rework.

13a: TLS 1.3 server handshake — DONE

Message construction (7b45b39bc7, 47254d7dea):

  • build_server_hello + build_encrypted_extensions
  • encode_certificate (Certificate presentation)
  • encode_certificate_verify — signing (ECDSA P-256 only for now;
    RSA-PSS needs a mbedtls_pk_sign_ext V wrapper that doesn't exist yet)
  • build_finished (verified against the real RFC 8448 §3 vector)
  • build_hello_retry_request

Server-side state machine (9ecb42eca7):

  • Tls13ServerHandshake (tls13_server_handshake.v) —
    respond_to_client_hello parses+fully validates an incoming
    ClientHello, does real ECDH, builds the entire response flight
    (ServerHello through this server's own Finished) in one call.
    process_finished verifies the client's Finished and confirms the
    handshake.
  • parse_client_hello + 4 supporting decoders added to
    tls13_client_hello.v (didn't exist before — only the client-side
    builder existed).

Every message-construction function round-trips through its
already-existing, independently-written parse counterpart (caught one real
bug this way — server key_share is a bare KeyShareEntry, not the
client's list-wrapped shape). The state machine is additionally verified by
a real client-vs-server integration test: a genuine
Tls13ClientHandshake and Tls13ServerHandshake, each independently
written against the RFC text, run against each other with fresh ECDHE keys
on both sides (not fixed vectors) — the client's own real
process_server_hello/process_encrypted_extensions/verify_finished all
independently accept this server's real output, and vice versa for the
client's Finished.

CertificateVerify's ECDSA signature is independently, cryptographically
verified (01f754809d)
via crypto.ecdsa.PublicKey.verify() — proving the
signed content, key, and DER encoding genuinely agree, plus that a
wrong-content/signature pairing and a wrong-key verification both correctly
fail. This is a same-library (OpenSSL signs, OpenSSL verifies) check, not
independent-library cross-verification the way a real peer's mbedTLS
eventually gets — this repo still has no EC certificate fixture to build an
mbedtls_pk_context from for that.

Known, deliberate scope limits (documented in code, not hidden):

  • CertificateVerify signing is ECDSA-P-256-only; RSA-PSS is explicitly
    rejected with a clear error, not silently mis-signed. No caller in this
    codebase uses RSA keys anywhere, so this is deliberately deferred rather
    than built speculatively.
  • Certificate/CertificateVerify chain verification is not exercised
    end-to-end (no EC certificate fixture in this repo) — the same gap Phase
    2c's own x509_standalone_signature_test.v documents for the identical
    reason.
  • HelloRetryRequest is not wired into the state machine — a key_share group
    mismatch is a hard failure. build_hello_retry_request exists and is
    unit-tested at the message layer. This is the SAME deliberate-defer scope
    choice Tls13ClientHandshake.process_server_hello already made for its
    own first-HRR gap (client side never generates a ClientHello2 either).

13b: Retry + address validation — DONE (d29851d6bc)

  • encode_retry_packet (retry.v) — builds a complete Retry packet,
    reusing compute_retry_integrity_tag directly (already side-agnostic
    — no new crypto needed for the tag). Round-trips through the
    already-existing, independently-written client-role
    verify_retry_integrity_tag/parse_retry_packet — proving the exact
    code that will receive this in production accepts it.
  • generate_retry_token/validate_retry_token/
    validate_retry_token_for_attempt (retry_token.v, new) —
    AEAD-sealed (AES-128-GCM) address-validation tokens. AEAD
    authentication alone satisfies RFC 9000 §8.1.4's "difficult to guess"
    and integrity requirements. validate_retry_token_for_attempt adds
    the two context-dependent checks §8.1.4 calls for: bound client
    address must match, and a short expiry window.
  • AntiAmplificationLimiter (anti_amplification.v, new) — RFC 9000
    §8.1's 3x pre-validation send limit, mirroring flow_control.v's
    FlowControlWindow shape. Standalone and tested; not yet wired into
    a datagram-processing loop (that's 13d's job).

Deliberate scope limits:

  • NEW_TOKEN-frame issuance (§8.1.3, tokens reusable across future
    connections) is out of scope — v1 only issues tokens via Retry.
  • Full single-use replay tracking beyond the short expiry window is
    deferred to 13d (needs a real listening socket to own a consumed-token
    cache's lifetime) — the short window satisfies §8.1.4's "prevented OR
    limited" replay requirement in the interim.

Found and flagged, not fixed here (separate task, out of scope for this
PR)
: while choosing a CSPRNG for the token nonce, discovered conn.v's
dial() uses V's general-purpose rand module (wyrand-backed, not
cryptographically secure) for original_dcid/scid/client_random
all security-relevant values that should use crypto.rand instead (identical
API, OS-backed, already used elsewhere in this codebase). This is a real
gap in already-merged code (Phase 9, part of #28129), not Phase 13 work —
now being tracked and worked in a separate session/branch.

13c: Connection ID lifecycle — DONE (bc5f108b9c)

  • NewConnectionIdFrame/RetireConnectionIdFrame wire codec
    (frame.v) — encode_new_connection_id_frame/
    parse_new_connection_id_frame and their RETIRE_CONNECTION_ID
    counterparts, types 0x18/0x19, previously falling through
    parse_frame's generic "not yet implemented" branch. Enforces the
    two frame-local RFC 9000 §19.15 requirements (retire_prior_to
    sequence_number; connection ID length in 1-20 bytes) on both the
    encode and decode sides, so a caller can't construct a frame this
    module's own parser would then reject. Every OTHER §19.15/§19.16
    requirement (zero-length-DCID prohibition, duplicate/conflicting
    sequence numbers, a RETIRE_CONNECTION_ID referencing the current
    packet's own DCID) needs connection state parse_frame doesn't
    have — deferred to the caller, the same division already
    established for HandshakeDoneFrame's role check.
  • generate_stateless_reset_token (stateless_reset.v) — RFC 9000
    §10.3.2's recommended construction, HMAC-SHA-256(static_key, connection_id) truncated to 16 bytes: a server-instance-local
    secret plus the connection ID deterministically reproduces the SAME
    token, so an endpoint that has lost all per-connection state (the
    entire premise of a stateless reset) can still recompute it.
    Cross-checked against StatelessResetTracker.is_stateless_reset
    (already-existing, independently-written matching logic) — proving
    a token this function generates is actually recognized by the exact
    code that would validate it in production.
  • QuicConn.dispatch_one_rtt_frame (conn.v) now explicitly
    acknowledges both new frame types instead of letting them fall
    silently into the generic informational-hint else-arm — found during
    self-review (a match QuicFrame consumer audit across the module):
    before this frame codec existed, these frame types could never reach
    that dispatch at all, so the else-arm's swallow of them only became
    a live, reachable code path as a direct consequence of this PR. Fixed
    inline rather than left implicit.

Deliberately still out of scope (per stateless_reset.v's own
long-standing note, unchanged by 13c): driving an ACTIVE SET of usable
connection IDs — issuing more as the peer retires them,
active_connection_id_limit accounting, CONNECTION_ID_LIMIT_ERROR
enforcement. That full lifecycle exists to support connection migration,
already listed below as a separate, explicitly deferrable follow-up — 13c
ships the wire codec and the token primitive it depends on, not the state
machine that would consume them.

13d-1: Server-role handshake wiring — DONE (3a97c5962f)

  • QuicConn gained real .server-role support: handshake split into
    ?&Tls13ClientHandshake (client) and a new server_handshake ?&Tls13ServerHandshake (server); role-aware directional key-selection
    helpers (own_initial_keys/peer_initial_keys/own_handshake_keys/
    peer_handshake_keys, RFC 9001 §5.2's client/server key swap);
    is_handshake_confirmed() branches per RFC 9001 §4.1.2 (client:
    HANDSHAKE_DONE received; server: handshake complete — the two are the
    SAME event only for the client).
  • dispatch_handshake_message gained a server branch
    (dispatch_server_handshake_message): bootstraps from the ClientHello
    via Tls13ServerHandshake.respond_to_client_hello, derives
    Handshake/Application keys, applies peer transport-parameter side
    effects — mirroring the client's existing per-state dispatch shape
    rather than a parallel implementation.
  • drain_outgoing now sends HANDSHAKE_DONE as soon as the server's
    handshake completes (RFC 9001 §4.1.2's "MUST send... as soon as the
    handshake is complete"), gated by a one-shot handshake_done_sent
    flag, then immediately calls on_handshake_confirmed — for a server,
    confirmed IS complete, so this ordering is non-circular.
  • New accept.v: pub fn accept(raw_datagram []u8, params AcceptParams, now u64) !(&QuicConn, PollResult), the server-role counterpart to
    dial(). Deliberately thin — bootstraps this connection's own
    identity and Initial-space keys from the client's first datagram, then
    hands that same datagram to the already-tested poll() for
    everything else, rather than a separate decrypt/dispatch path to keep
    in sync.

Two RFC-conformance bugs found and fixed via an adversarial multi-agent
review (4 independent lenses + an independent refutation pass on every
candidate finding) before this was pushed, both independently re-verified
by a second, focused agent after the fix:

  • RFC 9000 §7.2: a client's Initial-space packets address their DCID field
    to its own original_dcid until it has processed a reply from the
    server — the client can't address a value (the server's freshly generated
    scid) it hasn't learned yet. process_initial_or_handshake's DCID match
    now accepts a server-role connection's bootstrap ClientHello on that
    basis, scoped narrowly to role == .server && space == .initial.
  • RFC 9001 §4.9.1: Initial-key discard is send-triggered for a client
    ("MUST discard... when it first sends a Handshake packet") but
    receive-triggered for a server ("MUST discard... when it first
    successfully processes a Handshake packet"). The original diff applied
    the client's send-based trigger to both roles; since accept()'s single
    poll() call both processes the ClientHello AND sends the server's own
    Handshake-space response flight, this discarded the server's Initial keys
    before the client had sent anything back — silently dropping any
    ClientHello retransmission and leaving the server unable to
    PTO-retransmit its own lost first flight, stalling the handshake to idle
    timeout on ordinary first-round-trip packet loss. Fixed by splitting into
    a client-only send trigger (build_handshake_packet) and a new server
    receive trigger (process_initial_or_handshake, right after a
    Handshake-space packet from the client successfully decrypts).

Also closed a missing RFC 9000 §14.1 anti-amplification check found by the
same review: accept() now rejects any datagram under the 1200-byte floor
before doing any work, closing a reflection-amplification gap where a small,
address-spoofed trigger datagram could get a full multi-message response
flight.

Deliberate scope limits (documented in code, not hidden):

  • accept() does NOT decide Retry-vs-direct-accept policy (RFC 9000 §8.1) —
    that needs cross-connection-attempt state (has this address been seen
    before? is the anti-amplification budget exhausted?) only a caller
    tracking many attempts across many addresses can have. 13b's
    AntiAmplificationLimiter/Retry machinery already exists for 13d-2's
    listener to wire in before ever calling accept().
  • The server's Handshake-space CRYPTO flight is flushed as a single CRYPTO
    frame in a single packet — correct for this repo's own small test
    certificate, but not fragmented across multiple packets if a real-world
    certificate chain doesn't fit one packet's payload. dial()'s own
    ClientHello flush has the identical shape but was never previously
    exercised with anything large enough to expose it.

Out of scope for the remaining sub-phase: h3_server.v wiring (13e).
Connection migration (PATH_CHALLENGE/PATH_RESPONSE) is a separate,
explicitly deferrable follow-up. 0-RTT (Phase 14) and server push
(permanently disabled, RFC 9114 §7.2.7) stay out of scope regardless.

Test plan

  • ./vnew -no-memory-limit -silent test vlib/net/quic/ green (58/58)
  • Every new function covered by round-trip regression tests against its
    existing parse counterpart; build_finished additionally checked
    against the real RFC 8448 §3 Finished vector
  • Real client-vs-server integration test proving ECDH/key-schedule/
    Finished agreement between two independently-implemented roles
  • CertificateVerify's signature independently, cryptographically
    verified (positive + two negative cases)
  • Retry token: round-trip, randomization-per-call, wrong-key, tampered,
    expiry-boundary, and address-mismatch cases
  • Anti-amplification limiter: 3x boundary, accumulation, and
    post-validation unlimited cases
  • NEW_CONNECTION_ID/RETIRE_CONNECTION_ID: round-trip, both boundary
    checks (retire_prior_to > sequence_number, CID length outside 1-20)
    rejected on both encode and decode sides, truncated-buffer cases
  • Stateless reset token: determinism, differs per-CID and per-key,
    zero-length-CID rejection, and cross-checked end-to-end against
    StatelessResetTracker.is_stateless_reset
  • Self-reviewed pass before each push (Angle A-G, from-scratch contract
    check for each new function per Phase 0 step 6; 13c's pass included a
    full match QuicFrame consumer audit across the module, which caught
    the dispatch_one_rtt_frame gap listed above)
  • ./vnew fmt -w / ./vnew missdoc on touched files
  • 13d-1: real dial()/accept() integration test (accept_test.v) —
    genuine client-vs-server handshake including Certificate/
    CertificateVerify chain verification against a freshly generated EC
    cert (closes 13a's own documented gap) and a bidirectional stream
    exchange, all real byte-level datagrams, no internal shortcuts
  • 13d-1: adversarial multi-agent review (4 lenses + independent
    refutation pass) before push, surfacing the two RFC-conformance bugs
    and the anti-amplification gap described above; each fix independently
    re-verified by a second, focused adversarial pass after the fact
  • 13d-1: regression assertions pinning down both RFC-conformance fixes
    (Initial-key-discard timing at both the premature-discard and
    correct-discard points) plus a dedicated rejection test for the
    anti-amplification floor
  • External review round (not yet requested)

Draft — 13a, 13b, 13c, and 13d-1 complete; next up is 13d-2 (UDP listener +
connection demux).

Richard Wheeler and others added 10 commits August 24, 2026 13:37
Scoping pass mapping the completed client against what server support
needs is posted on the tracking issue (vlang#27675). Records the
suggested 13a-13e sub-phase breakdown in PROGRESS.md, mirroring Phase
12's own 12a-12d convention; 13a (TLS 1.3 server handshake) starts now.
First slice of the TLS 1.3 server handshake (Phase 13a): build_server_hello
and build_encrypted_extensions, the server-role mirror of the existing
client-role build_client_hello. Both round-trip through this file's own
parse_server_hello/parse_encrypted_extensions, which is a real cross-check
since build and parse were each derived independently from the RFC 8446
text rather than from each other.

Caught one real bug via the round-trip test before committing: a server's
key_share extension is a bare KeyShareEntry (RFC 8446 SS4.2.8), not the
list-wrapped KeyShareClientHello shape build_client_hello's own
encode_key_share_extension produces -- needed a dedicated
encode_key_share_extension_server. Same asymmetry already existed and was
handled correctly for supported_versions (bare 2 bytes server-side vs. a
length-prefixed list client-side); this is the identical class for
key_share, not a new discovery about the protocol.

build_encrypted_extensions enforces the two mandatory transport parameters
RFC 9000 SS7.3 requires an endpoint (initial_source_connection_id) and a
server specifically (original_destination_connection_id) to always include,
mirroring build_client_hello's identical enforcement of its own mandatory
field.

Still to come within 13a: Certificate presentation, CertificateVerify
signing, server Finished, HelloRetryRequest generation.
Completes the message-construction half of 13a's TLS 1.3 server handshake:

- encode_certificate (tls13_certificate.v): server Certificate message,
  reusing the existing CertificateEntry/ParsedCertificate types. certificate_
  request_context is always empty per RFC 8446 SS4.4.2 ("in the case of
  server authentication, this field SHALL be zero length").

- encode_certificate_verify (tls13_certificate.v): signs
  certificate_verify_signed_content(.server, ...) via crypto.ecdsa.PrivateKey.
  sign() -- a pre-existing V primitive, untouched by net.quic work so far.
  Only sig_scheme_ecdsa_secp256r1_sha256 is wired up; RSA-PSS signing is
  explicitly rejected with a clear error rather than silently mis-signing --
  it needs a mbedtls_pk_sign_ext V wrapper that doesn't exist yet (only the
  verify side, verify_rsa_pss_signature, does).

- build_finished (tls13_messages.v): thin wrapper around the already
  side-agnostic compute_finished_verify_data. Verified against the real RFC
  8448 SS3 vector, not just round-tripped against this module's own parser.

- build_hello_retry_request (tls13_server_hello.v): shares ServerHello's wire
  type, distinguished by the fixed magic random. key_share carries a bare
  NamedGroup (RFC 8446 SS4.2.8's KeyShareHelloRetryRequest) -- a third,
  distinct wire shape from both the client's and the real-ServerHello
  key_share encodings.

Every function round-trips through its already-existing, independently-
written parse counterpart -- a real cross-check, not tautological, since
build and parse were each derived from the RFC text separately. Full
net.quic suite 54/54, ./vnew missdoc clean, ./vnew fmt -w applied.

Certificate/CertificateVerify signature framing is verified; the produced
ECDSA signature's cryptographic validity is not cross-verified against an
independent verifier in this repo (no PublicKey.verify() in crypto.ecdsa, no
EC certificate fixture) -- the same documented gap Phase 2c's own
x509_standalone_signature_test.v already states for the identical reason.

Still to come within 13a: the server-side state machine wiring these five
functions into an actual handshake driver (see PROGRESS.md).
Tls13ServerHandshake (tls13_server_handshake.v), the server-role mirror of
the existing Tls13ClientHandshake. respond_to_client_hello parses and fully
validates an incoming ClientHello (cipher suite, TLS 1.3 offered, secp256r1
key_share, ecdsa_secp256r1_sha256 in signature_algorithms, ALPN common
protocol, and RFC 9000 SS7.3's transport-parameter role restrictions) BEFORE
allocating anything, does real ECDH against the client's offered key_share,
then builds the entire response flight -- ServerHello through this server's
own Finished -- in one call, since nothing arrives from the peer in between.
process_finished verifies the client's Finished and confirms the handshake.

Needed ClientHello parsing, which didn't exist at all before this commit:
added parse_client_hello, decode_alpn_offer, parse_key_share_extension_client,
parse_signature_algorithms_extension_client, and
parse_supported_versions_from_client to tls13_client_hello.v. Each client-vs-
server wire-shape asymmetry already established for ServerHello (list-wrapped
vs. bare key_share, versioned list vs. bare selected-version) recurs here in
reverse, documented the same way.

Verified with a real integration test, not just round-tripped against this
module's own code: a genuine Tls13ClientHandshake and Tls13ServerHandshake,
each independently written against the RFC text, run against each other with
fresh ECDHE keys on both sides (not fixed RFC 8448 vectors). The client's own
real process_server_hello/process_encrypted_extensions/verify_finished all
independently accept this server's real output, and this server's
process_finished accepts a real client Finished built the same way --
confirming the ECDH, key schedule, and both Finished computations genuinely
agree between two separately-implemented roles.

Full net.quic suite 55/55, ./vnew missdoc clean, ./vnew fmt -w applied.

Not exercised: Certificate/CertificateVerify chain verification end-to-end
(no EC certificate fixture in this repo -- same documented gap as
encode_certificate_verify's own tests); HelloRetryRequest generation is not
wired into this state machine (a key_share group mismatch is a hard failure)
-- the same deliberate-defer scope choice Tls13ClientHandshake.
process_server_hello already made for its own first-HRR gap.

This completes 13a's stated scope in PROGRESS.md. Next: 13b (Retry +
address validation).
Adds real cryptographic verification of encode_certificate_verify's output
via crypto.ecdsa's PublicKey.verify() -- which already exists (vlib/crypto/
ecdsa/ecdsa.v:340) and was previously missed by an incomplete grep for the
wrong receiver variable name, not actually absent. The prior round-trip test
only confirmed wire framing and non-constancy; this closes the real gap:
proving the signed content, key, and DER encoding all genuinely agree, not
just that the bytes look plausible.

Two new assertions: a wrong-transcript-hash/signature pairing must NOT
verify (rules out a check that ignores the content), and a signature must be
rejected by a DIFFERENT key's public half (rules out a verify() that accepts
anything). This is a same-library round trip (OpenSSL signs, OpenSSL
verifies) -- independent-library cross-verification via a real peer's
mbedTLS still isn't exercised, since this repo has no EC certificate fixture
to build an mbedtls_pk_context from. That remaining gap is unchanged and
still documented; only the previously-incorrect "no PublicKey.verify()
exists at all" claim is fixed.

Full net.quic suite 55/55.
encode_certificate_verify's doc comment stated as settled fact that its
OpenSSL-produced DER signature is directly compatible with net.mbedtls's
verify_ecdsa_signature ("no reformatting needed between the two
libraries"). That's the expected, standard behavior (OpenSSL's default EC
signing format and mbedTLS's ECDSA verification both use ASN.1 DER
ECDSA-Sig-Value, the TLS/X.509 convention), but this repo has never
actually tested it: there's no EC certificate fixture to build an
mbedtls_pk_context from for a real cross-library check, only a same-library
(OpenSSL signs, OpenSSL verifies) round trip. Reworded to state what's
actually verified (source inspection + the same-library test) versus what's
expected-but-untested, rather than asserting settled fact.

Caught during a requested pass verifying claims made across this PR's
commits/comments. Everything else checked (RSA-PSS signing wrapper
non-existence, EC certificate fixture non-existence, the three distinct
key_share wire shapes, the supported_versions client/server asymmetry, and
the application-secrets-derive-after-server-Finished timing) was confirmed
accurate against the cached primary RFC text directly.
Completes Phase 13b (Retry + address validation, RFC 9000 SS8/SS8.1/SS17.2.5):

- encode_retry_packet (retry.v): builds a complete Retry packet, reusing
  compute_retry_integrity_tag directly (already side-agnostic -- no new
  crypto needed for the tag itself). Round-trips through the already-
  existing, independently-written client-role verify_retry_integrity_tag/
  parse_retry_packet -- proving the exact code that will receive this in
  production actually accepts it, not just that it looks well-formed.

- generate_retry_token/validate_retry_token/validate_retry_token_for_attempt
  (retry_token.v, new): AEAD-sealed (AES-128-GCM) address-validation tokens.
  AEAD authentication alone satisfies RFC 9000 SS8.1.4's "difficult to
  guess" and integrity requirements -- no separate random component needed
  beyond the nonce GCM itself requires. validate_retry_token_for_attempt
  adds the two context-dependent checks SS8.1.4 calls for: bound client
  address must match, and a short expiry window. NEW_TOKEN-frame issuance
  (SS8.1.3, tokens reusable across future connections) is out of scope --
  v1 only issues tokens via Retry. Full single-use replay tracking beyond
  the expiry window is deferred to 13d (needs a real listening socket to
  own a consumed-token cache's lifetime); the short window satisfies
  SS8.1.4's "prevented OR limited" replay requirement in the interim.

- AntiAmplificationLimiter (anti_amplification.v, new): RFC 9000 SS8.1's 3x
  pre-validation send limit, deliberately mirroring flow_control.v's
  FlowControlWindow shape. Standalone and tested; not yet wired into any
  datagram-processing loop, since that loop doesn't exist until 13d.

Found and flagged (not fixed here, out of scope): while choosing a CSPRNG
for the token nonce, discovered conn.v's dial() uses V's general-purpose
`rand` module (wyrand-backed, not cryptographically secure) for
original_dcid/scid/client_random -- all security-relevant values that
should use crypto.rand instead (identical API, OS-backed, already used
elsewhere in this codebase). Real gap in already-merged Phase 9 code
(PR vlang#28129), flagged as a separate follow-up task.

Full net.quic suite 57/57, ./vnew missdoc clean, ./vnew fmt -w applied.
…NECTION_ID)

Adds the RFC 9000 §19.15/§19.16 wire codec for NEW_CONNECTION_ID/
RETIRE_CONNECTION_ID frames (frame.v), previously falling through
parse_frame's generic "not yet implemented" branch, plus
generate_stateless_reset_token (stateless_reset.v) implementing RFC 9000
§10.3.2's recommended HMAC-SHA-256(static_key, connection_id) derivation,
cross-checked against the existing StatelessResetTracker.is_stateless_reset.

Also updates QuicConn.dispatch_one_rtt_frame (conn.v) to explicitly
acknowledge the two new frame types now flow through it instead of
silently landing in the generic informational-hint else-arm, documenting
why they're accepted-but-not-yet-acted-upon pending the (deliberately
deferred) active-connection-ID-set state machine.

Driving that active set (issuing more CIDs, active_connection_id_limit
accounting) remains out of scope, same as before this commit.
…o QuicConn

Adds server-role support to QuicConn (previously client-only) plus a new
accept() constructor mirroring dial(): role-aware directional key
selection, role-branched handshake dispatch (dispatch_server_handshake_message),
role-asymmetric handshake-confirmation semantics (RFC 9001 §4.1.2), and
server-side HANDSHAKE_DONE sending -- all reusing the existing client-role
packet-building/CRYPTO-reassembly/drain machinery.

Two RFC-conformance bugs found and fixed via an adversarial multi-agent
review before commit, both independently re-verified:

- RFC 9000 §7.2: a client's Initial-space packets address their DCID field
  to its own original_dcid until it has processed a reply, so the DCID
  match in process_initial_or_handshake now accepts a server-role
  connection's bootstrap ClientHello even though the server's real scid
  didn't exist yet when the client sent it.
- RFC 9001 §4.9.1: Initial-key discard is send-triggered for a client but
  receive-triggered for a server -- applying the client's trigger to both
  roles made accept()'s single poll() call discard the server's Initial
  keys before the client had sent anything back, silently dropping any
  ClientHello retransmission and blocking the server's own PTO
  retransmission of its first flight. Split into a client-only send
  trigger (build_handshake_packet) and a new server receive trigger
  (process_initial_or_handshake, right after a Handshake-space packet
  from the client decrypts).

Also fixes a missing RFC 9000 §14.1 anti-amplification check: accept()
now rejects any datagram under the 1200-byte floor before doing any work,
closing a reflection-amplification gap.

accept() deliberately does not decide Retry-vs-direct-accept policy
(deferred to 13d-2's UDP listener, which has the cross-connection-attempt
state that decision needs) and does not fragment a large certificate
chain's Handshake-space CRYPTO flight across multiple packets (documented
scope limits, not blockers for this repo's own small test certificate).

Tests: new accept_test.v drives a real dial()/accept() pair through a
full handshake (including real Certificate/CertificateVerify chain
verification against a freshly generated EC cert -- 13a's own PROGRESS.md
noted gap) and bidirectional stream exchange, plus regression assertions
for both RFC-conformance fixes above and a rejection test for the
anti-amplification floor. Full suite: 58/58 passing.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Documentation-only update following 3a97c59.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@quaesitor-scientiam quaesitor-scientiam changed the title net.quic/net.http: HTTP/3 server support, Phase 13a (TLS 1.3 server handshake) net.quic/net.http: HTTP/3 server support, Phase 13 (13a-13d-1) Aug 25, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant